using Box2DSharp.Common;
using JetBrains.Annotations;
using Newtonsoft.Json;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Unity.Collections;
using Unity.VisualScripting;
using UnityEngine;


namespace cframework.lockstep
{
    class WaitForSecond_LS : YieldInstruction
    {
        FP m_Seconds;

        public WaitForSecond_LS(FP seconds)
        {
            m_Seconds = seconds;
        }

        public bool CanContinue(FP waitStartTime, FP currentTime)
        {
            return currentTime - waitStartTime >= m_Seconds;
        }
    }

    public class LockStepWorld
    {
        public static LockStepWorld current
        {
            get;
            private set;
        }

        class TimerInfo
        {
            public System.Func<bool> callback;
            public object o;
            public FP nextTime;
            public FP delay;
            public int repeatCount;
        }

        public interface ICoroutineInfo
        {
        }

        class CoroutineInfo : ICoroutineInfo
        {
            public IEnumerator e;
            public MonoBehaviour o;
            public CoroutineInfo waitCoroutine;
            public FP lastTime;

            public bool Process()
            {
                var c = this;

                if (c.o != null)
                {
                    if (c.o.IsDestroyed())
                    {
                        return false;
                    }

                    if (!c.o.enabled)
                    {
                        return true;
                    }
                }

                bool canContinue = c.e.Current == null;

                if (c.waitCoroutine != null)
                {
                    if (c.waitCoroutine.Process())
                    {
                        return true;
                    }

                    c.waitCoroutine = null;
                    canContinue = true;
                }

                if (!canContinue)
                {
                    var w = c.e.Current as WaitForSecond_LS;
                    if (w != null)
                    {
                        canContinue = w.CanContinue(c.lastTime, LockStepWorld.current.time);
                    }
                    else
                    {
                        canContinue = true;
                    }
                }

                if (canContinue)
                {
                    bool result = false;
                    try
                    {
                        result = c.e.MoveNext();
                    }
                    catch (System.Exception e)
                    {
                        Debug.LogException(e);
                    }

                    if (result)
                    {
                        c.lastTime = LockStepWorld.current.time;

                        if (c.e.Current != null && c.e.Current is IEnumerator)
                        {
                            waitCoroutine = new CoroutineInfo();
                            waitCoroutine.lastTime = LockStepWorld.current.time;
                            waitCoroutine.e = c.e.Current as IEnumerator;
                        }
                    }
                    else
                    {
                        return false;
                    }
                }

                return true;
            }
        }

        public delegate void ProcessServerStepCallback(int _serverFrameIndex, FP serverTime, FP deltaTime, object msgObj);
        public ProcessServerStepCallback processServerStepCallback;

        public delegate void ProcessWaitMsgStepCallback(float clientTime, float deltaTime);
        public ProcessWaitMsgStepCallback processWaitMsgStepCallback;

        public delegate void ProcessClientStepCallback(int _serverFrameIndex, FP serverTime, FP deltaTime);
        public ProcessClientStepCallback processClientStepCallback;

        MsgQueue<double> frameStartTimes = new MsgQueue<double>(30);
        public bool ignoreServerMsg;


        public int clientHerz = 30;
        FP clientInterval;
        FP clientNextStepTime;
        FP clientLastStepTime;
        int clientFrameIndex;

        public int serverDelay = 0; //millisecond
        public int serverHerz = 15;
        public int serverMsgBeginIndex;
        FP serverInterval;
        FP serverNextStepTime;
        FP serverLastStepTime;
        int serverFrameIndex;
        int serverAddMsgIndex;
        int maxServerFrameIndex;

        public int delay = 0; //millisecond
        FP interval;
        FP lastStepTime;
        FP nextStepTime;
        public double startTime
        {
            get;
            private set;
        }

        double idealStartTime;

        double msgDealTime;
        double minStartTime;
        double sumStartTime = 0;

        Dictionary<int, object> msgs;
        Dictionary<int, double> msgAddedTimes = new Dictionary<int, double>();
        Dictionary<int, double> msgWaitStartTimes = new Dictionary<int, double>();
        List<TimerInfo> timerInfos = new List<TimerInfo>();
        List<CoroutineInfo> coroutines = new List<CoroutineInfo>();
        SortedDictionary<int, List<System.Action>> updates = new SortedDictionary<int, List<System.Action>>();
        SortedDictionary<int, List<System.Action>> lateUpdates = new SortedDictionary<int, List<System.Action>>();
        Dictionary<int, double> startTimeMap = new Dictionary<int, double>();


        public FP time
        {
            get
            { return clientNextStepTime; }
        }

        public FP deltaTime
        {
            get
            { return clientNextStepTime - clientLastStepTime; }
        }

        public int GetRemainFrameCount()
        {
            if (msgs == null)
            {
                return 0;
            }

            return msgs.Count;
        }

        public void SetHerz(int _clientHerz, int _serverHerz = 0)
        {
            if (_clientHerz > 0)
            {
                this.clientHerz = _clientHerz;
                this.clientInterval = (FP)1 / _clientHerz;
            }

            if (_serverHerz > 0)
            {
                this.serverHerz = _serverHerz;
                this.serverInterval = (FP)1 / _serverHerz;
            }

            this.interval = (FP)1 / FMath.Max(this.clientHerz, this.serverHerz);
        }

        public void DesstroyWorld()
        {
            msgs = null;
            timerInfos.Clear();
            msgAddedTimes.Clear();
            msgWaitStartTimes.Clear();
            coroutines.Clear();
            updates.Clear();
            lateUpdates.Clear();
            frameStartTimes.Reset();
            startTimeMap.Clear();
        }

        public bool StartWorld()
        {
            if (msgs != null)
            {
                return false;
            }

            int _clientHerz = clientHerz;
            int _serverHerz = serverHerz;
            int _serverMsgBeginIndex = serverMsgBeginIndex;

            if (_serverHerz <= 0 || _clientHerz <= 0)
            {
                return false;
            }

            serverMsgBeginIndex = _serverMsgBeginIndex;
            maxServerFrameIndex = _serverMsgBeginIndex - 1;
            serverHerz = _serverHerz;
            serverInterval = (FP)1 / serverHerz;
            serverLastStepTime = serverNextStepTime = serverDelay / (FP)1000;
            serverFrameIndex = 0;
            serverAddMsgIndex = serverMsgBeginIndex;
            idealStartTime = 0;
            minStartTime = 0;
            sumStartTime = 0;

            clientHerz = _clientHerz;
            clientInterval = (FP)1 / clientHerz;
            clientLastStepTime = clientNextStepTime = 0;
            clientFrameIndex = 0;

            startTime = Time.realtimeSinceStartupAsDouble + delay / 1000d;
            nextStepTime = lastStepTime = 0;
            interval = (FP)1 / Mathf.Max(serverHerz, clientHerz);

            msgs = new Dictionary<int, object>();
            return true;
        }

        public bool isStarted()
        {
            return msgs != null;
        }



        public void UpdateStep(double maxProcessTime = 0)
        {
            if (msgs == null)
            {
                return;
            }

            var startProcessStepTime = Time.realtimeSinceStartupAsDouble;

            while (maxProcessTime <= 0 || Time.realtimeSinceStartupAsDouble - startProcessStepTime < maxProcessTime)
            {
                var realTimeSinceStartUp = Time.realtimeSinceStartupAsDouble;
                var realDiffTime = realTimeSinceStartUp - startTime;
                if (realDiffTime < nextStepTime)
                {
                    break;
                }

                while (nextStepTime >= FP.Min(serverNextStepTime, clientNextStepTime))
                {
                    if (serverNextStepTime <= clientNextStepTime)
                    {
                        //process server step

                        object msgObj = null;
                        if (!ignoreServerMsg && serverFrameIndex >= serverMsgBeginIndex)
                        {
                            //if server frame not found return
                            if (!msgs.TryGetValue(serverFrameIndex, out msgObj))
                            {
                                if (msgs.Count <= 0)
                                {
                                    var deltaTime = Mathf.Min((float)realDiffTime - (float)clientLastStepTime, (float)clientInterval);
                                    try
                                    {
                                        ProcessWaitMsgStep((float)clientLastStepTime + deltaTime, deltaTime);
                                    }
                                    catch (System.Exception e)
                                    {
                                        Debug.LogException(e);
                                    }
                                }

                                return;
                            }
                            msgs.Remove(serverFrameIndex);


                        }
                        var oldfsg = LockStepWorld.current;
                        LockStepWorld.current = this;

                        try
                        {
                            ProcessServerStep(serverFrameIndex, serverNextStepTime, serverNextStepTime - serverLastStepTime, msgObj);
                        }
                        catch (System.Exception e)
                        {
                            Debug.LogException(e);
                        }

                        LockStepWorld.current = oldfsg;

                        serverFrameIndex++;
                        serverLastStepTime = serverNextStepTime;
                        serverNextStepTime += serverInterval;
                    }
                    else
                    {
                        var oldfsg = LockStepWorld.current;
                        LockStepWorld.current = this;

                        ProcessCoroutines();
                        ProcessTimers();

                        //process client step
                        try
                        {
                            ProcessClientStep(clientFrameIndex, clientNextStepTime, clientNextStepTime - clientLastStepTime);
                        }
                        catch (System.Exception e)
                        {
                            Debug.LogException(e);
                        }

                        ProcessUpdates();
                        ProcessLateUpdates();

                        LockStepWorld.current = oldfsg;

                        clientFrameIndex++;
                        clientLastStepTime = clientNextStepTime;
                        clientNextStepTime += clientInterval;
                    }
                }

                lastStepTime = nextStepTime;
                nextStepTime += interval;
            }

        }

        public double GetMsgDealTime()
        {
            return msgDealTime;
        }

        public void AddUpdate(System.Action u, int order = 0)
        {
            if (this.msgs == null)
            {
                return;
            }

            List<System.Action> ulist = null;
            if (!this.updates.TryGetValue(order, out ulist))
            {
                ulist = new List<System.Action>();
                this.updates.Add(order, ulist);
            }

            ulist.Add(u);
        }

        public bool RemoveUpdate(System.Action u, int order = 0)
        {
            if (this.msgs == null)
            {
                return false;
            }

            List<System.Action> ulist = null;
            if (this.updates.TryGetValue(order, out ulist))
            {
                return ulist.Remove(u);
            }

            return false;
        }

        protected void ProcessUpdates()
        {
            var e = this.updates.GetEnumerator();
            while (e.MoveNext())
            {
                var us = e.Current.Value;
                for (int i = us.Count - 1; i >= 0; i--)
                {
                    var u = us[i];
                    if (u.Target is UnityEngine.Object)
                    {
                        var o = u.Target as UnityEngine.Object;
                        if (o == null)
                        {
                            us.RemoveAt(i);
                            continue;
                        }
                    }

                    try
                    {
                        u();
                    }
                    catch (System.Exception ex)
                    {
                        Debug.LogException(ex);
                    }
                }
            }
        }

        public void AddLateUpdate(System.Action u, int order = 0)
        {
            if (this.msgs == null)
            {
                return;
            }

            List<System.Action> ulist = null;
            if (!this.lateUpdates.TryGetValue(order, out ulist))
            {
                ulist = new List<System.Action>();
                this.lateUpdates.Add(order, ulist);
            }

            ulist.Add(u);
        }

        public bool RemoveLateUpdate(System.Action u, int order = 0)
        {
            if (this.msgs == null)
            {
                return false;
            }

            List<System.Action> ulist = null;
            if (this.lateUpdates.TryGetValue(order, out ulist))
            {
                return ulist.Remove(u);
            }

            return false;
        }

        protected void ProcessLateUpdates()
        {
            var e = this.lateUpdates.GetEnumerator();
            while (e.MoveNext())
            {
                var us = e.Current.Value;
                for (int i = us.Count - 1; i >= 0; i--)
                {
                    var u = us[i];
                    if (u.Target is UnityEngine.Object)
                    {
                        var o = u.Target as UnityEngine.Object;
                        if (o == null)
                        {
                            us.RemoveAt(i);
                            continue;
                        }
                    }

                    try
                    {
                        u();
                    }
                    catch (System.Exception ex)
                    {
                        Debug.LogException(ex);
                    }
                }
            }
        }

        public ICoroutineInfo StartCoroutine(IEnumerator e, MonoBehaviour o = null)
        {
            if (msgs == null)
            {
                return null;
            }

            CoroutineInfo c = new CoroutineInfo();
            c.e = e;


            c.lastTime = clientNextStepTime;
            c.o = o;

            coroutines.Add(c);

            return c;
        }

        public void StopCoroutine(ICoroutineInfo e)
        {
            if (msgs == null)
            {
                return;
            }

            var c = e as CoroutineInfo;
            if (c == null)
            {
                return;
            }

            coroutines.Remove(c);
        }

        public void ProcessCoroutines()
        {
            for (int i = coroutines.Count - 1; i >= 0; i--)
            {
                var c = coroutines[i];

                if (!c.Process())
                {
                    coroutines.Remove(c);
                }
            }
        }

        public object StartTimer(object obj, System.Func<bool> callback, FP delay, int repeat = 1)
        {
            if (msgs == null)
            {
                return null;
            }

            if (repeat == 0)
            {
                return null;
            }

            var callbackTime = clientNextStepTime + delay;
            var cbi = new TimerInfo() { callback = callback, o = obj, nextTime = callbackTime, delay = delay, repeatCount = repeat };
            timerInfos.Add(cbi);
            return cbi;
        }

        public bool StopTimer(object cbi)
        {
            if (msgs == null)
            {
                return false;
            }

            return timerInfos.Remove(cbi as TimerInfo);
        }

        protected virtual void ProcessTimers()
        {
            for (int i = timerInfos.Count - 1; i >= 0; i--)
            {
                var c = timerInfos[i];
                while (this.time >= c.nextTime)
                {
                    bool result = false;
                    var obj = c.o ?? c.callback.Target;
                    if (obj is UnityEngine.Object)
                    {
                        var o = obj as UnityEngine.Object;
                        if (o == null)
                        {
                            timerInfos.RemoveAt(i);
                            break;
                        }
                    }

                    try
                    {
                        result = c.callback();
                    }
                    catch (System.Exception e)
                    {
                        Debug.LogException(e);
                    }

                    bool removed = false;
                    c.nextTime += c.delay;
                    if (c.repeatCount > 0)
                    {
                        c.repeatCount--;
                        if (c.repeatCount == 0)
                        {
                            timerInfos.RemoveAt(i);
                            break;
                        }
                    }

                    if (!result && !removed)
                    {
                        timerInfos.RemoveAt(i);
                        break;
                    }
                }
            }
        }


        protected virtual void ProcessWaitMsgStep(float clientTime, float deltaTime)
        {

            processWaitMsgStepCallback?.Invoke(clientTime, deltaTime);
        }
        protected virtual void ProcessServerStep(int _serverFrameIndex, FP serverTime, FP deltaTime, object msgObj)
        {

            processServerStepCallback?.Invoke(_serverFrameIndex, serverTime, deltaTime, msgObj);
        }

        protected virtual void ProcessClientStep(int _clientFrameIndex, FP clientTime, FP deltaTime)
        {

            processClientStepCallback?.Invoke(_clientFrameIndex, clientTime, deltaTime);
        }

        public bool AddMsg(int index, object m)
        {
            if (msgs == null)
            {
                return false;
            }

            if (msgs.ContainsKey(index) || index < serverFrameIndex)
            {
                return false;
            }


            msgs.Add(index, m);

            var nowTime = Time.realtimeSinceStartupAsDouble;
            var needServerTime = index * (double)serverInterval;
            var needStartTime = nowTime - needServerTime;

            minStartTime = (minStartTime == 0 || needStartTime < minStartTime) ? needStartTime : minStartTime;

            if (minStartTime + 1 < needStartTime)
            {
                needStartTime = minStartTime + 1;
            }

            if (serverAddMsgIndex == index)
            {
                serverAddMsgIndex++;

                if (frameStartTimes.Length() >= frameStartTimes.Capility)
                {
                    var oldFrameStartTime = frameStartTimes.Dequeue();
                    sumStartTime -= oldFrameStartTime;


                }

                frameStartTimes.Enqueue(needStartTime);
                sumStartTime += needStartTime;


                double nextFrameStartTime = 0;
                while (startTimeMap.Remove(serverAddMsgIndex, out nextFrameStartTime))
                {
                    serverAddMsgIndex++;

                    if (frameStartTimes.Length() >= frameStartTimes.Capility)
                    {
                        var oldFrameStartTime = frameStartTimes.Dequeue();
                        sumStartTime -= oldFrameStartTime;
                    }

                    frameStartTimes.Enqueue(nextFrameStartTime);
                    sumStartTime += nextFrameStartTime;
                }
            }
            else
            {
                startTimeMap.Add(index, needStartTime);
            }


            //msgAddedTimes.Add(index, t);

            if (index > maxServerFrameIndex)
            {
                maxServerFrameIndex = index;
            }

            this.idealStartTime = sumStartTime / frameStartTimes.Length();
            this.startTime = idealStartTime;

            return true;
        }

    }
}
